Skip to main content

Cloud Formation Template

The application is deploying using an AWS Cloud Formation Template that comes bundled with the AMI machine image we provide. This template manages the setup of all the resources necessary to support the application and creates the necessary roles and role assignments consistent with current best practices. You should not need to adjust the template but the contents are provide here for your reference.

AWSTemplateFormatVersion: '2010-09-09'
Description: AWS Marketplace Central - Complete FTR Stack (VPC, Storage, Compute, Security)

Resources:
# ==========================================
# NETWORKING
# ==========================================
MarketplaceVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: MarketplaceVPC

PrivateSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref MarketplaceVPC
CidrBlock: 10.0.10.0/24
AvailabilityZone: !Select [0, !GetAZs '']
Tags:
- Key: Name
Value: PrivateSubnetA

PrivateSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref MarketplaceVPC
CidrBlock: 10.0.11.0/24
AvailabilityZone: !Select [1, !GetAZs '']
Tags:
- Key: Name
Value: PrivateSubnetB

PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref MarketplaceVPC
Tags:
- Key: Name
Value: PrivateRouteTable

PrivateSubnetARouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnetA
RouteTableId: !Ref PrivateRouteTable

PrivateSubnetBRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnetB
RouteTableId: !Ref PrivateRouteTable

VPCEndpointSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for VPC endpoints
VpcId: !Ref MarketplaceVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 10.0.0.0/16

DynamoDBEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref MarketplaceVPC
ServiceName: !Sub com.amazonaws.${AWS::Region}.dynamodb
VpcEndpointType: Gateway
RouteTableIds:
- !Ref PrivateRouteTable

S3Endpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref MarketplaceVPC
ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
VpcEndpointType: Gateway
RouteTableIds:
- !Ref PrivateRouteTable

BedrockRuntimeEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref MarketplaceVPC
ServiceName: !Sub com.amazonaws.${AWS::Region}.bedrock-runtime
VpcEndpointType: Interface
PrivateDnsEnabled: true
SubnetIds:
- !Ref PrivateSubnetA
- !Ref PrivateSubnetB
SecurityGroupIds:
- !Ref VPCEndpointSecurityGroup

KMSEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref MarketplaceVPC
ServiceName: !Sub com.amazonaws.${AWS::Region}.kms
VpcEndpointType: Interface
PrivateDnsEnabled: true
SubnetIds:
- !Ref PrivateSubnetA
- !Ref PrivateSubnetB
SecurityGroupIds:
- !Ref VPCEndpointSecurityGroup

SecretsManagerEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref MarketplaceVPC
ServiceName: !Sub com.amazonaws.${AWS::Region}.secretsmanager
VpcEndpointType: Interface
PrivateDnsEnabled: true
SubnetIds:
- !Ref PrivateSubnetA
- !Ref PrivateSubnetB
SecurityGroupIds:
- !Ref VPCEndpointSecurityGroup

CloudWatchLogsEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref MarketplaceVPC
ServiceName: !Sub com.amazonaws.${AWS::Region}.logs
VpcEndpointType: Interface
PrivateDnsEnabled: true
SubnetIds:
- !Ref PrivateSubnetA
- !Ref PrivateSubnetB
SecurityGroupIds:
- !Ref VPCEndpointSecurityGroup

STSEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref MarketplaceVPC
ServiceName: !Sub com.amazonaws.${AWS::Region}.sts
VpcEndpointType: Interface
PrivateDnsEnabled: true
SubnetIds:
- !Ref PrivateSubnetA
- !Ref PrivateSubnetB
SecurityGroupIds:
- !Ref VPCEndpointSecurityGroup

# ==========================================
# SECURITY
# ==========================================
LambdaSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for VPC Lambda functions
VpcId: !Ref MarketplaceVPC
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0

# ==========================================
# STORAGE
# ==========================================

# DynamoDB Tables
AgreementsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: MarketplaceAgreements
AttributeDefinitions:
- AttributeName: PK
AttributeType: S
- AttributeName: SK
AttributeType: S
- AttributeName: GSI1PK
AttributeType: S
- AttributeName: GSI1SK
AttributeType: S
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
BillingMode: PAY_PER_REQUEST
GlobalSecondaryIndexes:
- IndexName: GSI1
KeySchema:
- AttributeName: GSI1PK
KeyType: HASH
- AttributeName: GSI1SK
KeyType: RANGE
Projection:
ProjectionType: ALL

OffersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: MarketplaceOffers
AttributeDefinitions:
- AttributeName: offerId
AttributeType: S
- AttributeName: recordType
AttributeType: S
KeySchema:
- AttributeName: offerId
KeyType: HASH
- AttributeName: recordType
KeyType: RANGE
BillingMode: PAY_PER_REQUEST

VendorsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: MarketplaceVendors
AttributeDefinitions:
- AttributeName: PK
AttributeType: S
- AttributeName: SK
AttributeType: S
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
BillingMode: PAY_PER_REQUEST

TransactionsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: MarketplaceTransactions
AttributeDefinitions:
- AttributeName: PK
AttributeType: S
- AttributeName: SK
AttributeType: S
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
BillingMode: PAY_PER_REQUEST
TimeToLiveSpecification:
AttributeName: expiresAt
Enabled: true

PrivateOfferRequestsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: MarketplacePrivateOfferRequests
AttributeDefinitions:
- AttributeName: requestId
AttributeType: S
KeySchema:
- AttributeName: requestId
KeyType: HASH
BillingMode: PAY_PER_REQUEST

MarketplaceRecommendations:
Type: AWS::DynamoDB::Table
Properties:
TableName: MarketplaceRecommendations
BillingMode: PAY_PER_REQUEST
DeletionProtectionEnabled: true
PointInTimeRecoverySpecification:
PointInTimeRecoveryEnabled: true
SSESpecification:
SSEEnabled: true
SSEType: KMS
KMSMasterKeyId: alias/marketplace/dynamodb
TimeToLiveSpecification:
AttributeName: expiresAt
Enabled: true
AttributeDefinitions:
- AttributeName: PK
AttributeType: S
- AttributeName: SK
AttributeType: S
- AttributeName: vendorId
AttributeType: S
- AttributeName: status
AttributeType: S
- AttributeName: solutionId
AttributeType: S
- AttributeName: createdAt
AttributeType: S
- AttributeName: GSI3SK
AttributeType: S
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
GlobalSecondaryIndexes:
- IndexName: GSI1-VendorStatus
KeySchema:
- AttributeName: vendorId
KeyType: HASH
- AttributeName: status
KeyType: RANGE
Projection:
ProjectionType: ALL
- IndexName: GSI2-SolutionCreatedAt
KeySchema:
- AttributeName: solutionId
KeyType: HASH
- AttributeName: createdAt
KeyType: RANGE
Projection:
ProjectionType: ALL
- IndexName: GSI3-AccountSaved
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: GSI3SK
KeyType: RANGE
Projection:
ProjectionType: ALL
Tags:
- Key: Project
Value: MarketplaceIntegration
- Key: Environment
Value: Production
- Key: ManagedBy
Value: CloudFormation

# S3 Buckets
DocumentsBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub "marketplace-documents-${AWS::AccountId}-${AWS::Region}"
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: aws:kms
KMSMasterKeyID: alias/marketplace/s3

DocumentsBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref DocumentsBucket
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: EnforceTLS
Effect: Deny
Principal: "*"
Action: "s3:*"
Resource:
- !Sub "${DocumentsBucket.Arn}"
- !Sub "${DocumentsBucket.Arn}/*"
Condition:
Bool:
"aws:SecureTransport": "false"

FrontendBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub "marketplace-frontend-${AWS::AccountId}-${AWS::Region}"
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: index.html
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256

FrontendBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref FrontendBucket
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: PublicReadGetObject
Effect: Allow
Principal: "*"
Action: "s3:GetObject"
Resource: !Sub "${FrontendBucket.Arn}/*"

# ==========================================
# COMPUTE
# ==========================================

# IAM Roles
AgreementsFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

OffersFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

VendorsFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

TransactionsFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

DiscoveryFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

DocumentsFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole

SyncFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

PrivateOfferRequestFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

# Lambda Functions
AgreementsFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: AgreementsFunction
Handler: index.handler
Role: !GetAtt AgreementsFunctionRole.Arn
Code:
ZipFile: |
exports.handler = async (event) => {
return { statusCode: 200, body: JSON.stringify('Agreements Function') };
};
Runtime: nodejs20.x

OffersFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: OffersFunction
Handler: index.handler
Role: !GetAtt OffersFunctionRole.Arn
Code:
ZipFile: |
exports.handler = async (event) => {
return { statusCode: 200, body: JSON.stringify('Offers Function') };
};
Runtime: nodejs20.x

VendorsFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: VendorsFunction
Handler: index.handler
Role: !GetAtt VendorsFunctionRole.Arn
Code:
ZipFile: |
exports.handler = async (event) => {
return { statusCode: 200, body: JSON.stringify('Vendors Function') };
};
Runtime: nodejs20.x

TransactionsFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: TransactionsFunction
Handler: index.handler
Role: !GetAtt TransactionsFunctionRole.Arn
Code:
ZipFile: |
exports.handler = async (event) => {
return { statusCode: 200, body: JSON.stringify('Transactions Function') };
};
Runtime: nodejs20.x

DiscoveryFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: DiscoveryFunction
Handler: index.handler
Role: !GetAtt DiscoveryFunctionRole.Arn
Code:
ZipFile: |
exports.handler = async (event) => {
console.log("Asking Marketplace MCP");
return { statusCode: 200, body: JSON.stringify('Discovery Function') };
};
Runtime: nodejs20.x

DocumentsFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: DocumentsFunction
Handler: index.handler
Role: !GetAtt DocumentsFunctionRole.Arn
Code:
ZipFile: |
exports.handler = async (event) => {
return { statusCode: 200, body: JSON.stringify('Documents Function') };
};
Runtime: nodejs20.x
VpcConfig:
SecurityGroupIds:
- !Ref LambdaSecurityGroup
SubnetIds:
- !Ref PrivateSubnetA
- !Ref PrivateSubnetB

SyncFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: SyncFunction
Handler: index.handler
Role: !GetAtt SyncFunctionRole.Arn
Code:
ZipFile: |
exports.handler = async (event) => {
console.log("Syncing agreements/offers");
return { statusCode: 200, body: JSON.stringify('Sync Function') };
};
Runtime: nodejs20.x

PrivateOfferRequestFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: PrivateOfferRequestFunction
Handler: index.handler
Role: !GetAtt PrivateOfferRequestFunctionRole.Arn
Code:
ZipFile: |
exports.handler = async (event) => {
console.log("Sending private offer email");
return { statusCode: 200, body: JSON.stringify('Private Offer Request Sent') };
};
Runtime: nodejs20.x

Outputs:
VPCId:
Value: !Ref MarketplaceVPC
AgreementsTableName:
Value: !Ref AgreementsTable
DocumentsBucketName:
Value: !Ref DocumentsBucket
FrontendURL:
Description: URL for the static website frontend
Value: !GetAtt FrontendBucket.WebsiteURL